Skip to content

Extension is workflow-class identity; nothing carries it per run - #4

Merged
czpython merged 12 commits into
mainfrom
extension-class-identity
Jul 13, 2026
Merged

Extension is workflow-class identity; nothing carries it per run#4
czpython merged 12 commits into
mainfrom
extension-class-identity

Conversation

@czpython

@czpython czpython commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Supersedes #3 (folded in — this PR now carries the whole arc against main). Two moves that end in one place: identity lives where it's declared, not on the run.

What changed

  • Subject keying rides DBOS custom workflow attributes (the former Subject keying rides DBOS workflow attributes; durable_runs shrinks #3): start() stamps subject_type/subject_id via SetWorkflowAttributes, subject_filter() answers "runs for this subject" off workflow_status alone, and durable_runs sheds its subject/input/extension columns (bare drops, no backfill — pre-2.26 hosts don't exist). Extensions read runs through Run.list_for_subject(..., kind=), never druks.durable internals. DBOS workflow-status GC/retention must stay off.
  • A workflow can only belong to the extension that declares it, so extension identity moves off the run entirely — no start() argument, no DBOS attribute, no column, no read-back:
  • Ownership registers before import. The loader registers every entry point's package (and the class's Extension.package) before importing extension modules; Workflow.__init_subclass__ resolves cls.extension from the registry and fails loudly for an unowned module, with the loader invariant in the message.
  • The declaring extension namespaces kind, universally. In-tree kinds are byte-identical (build.*, core.*, usage.* — pinned by test). Out-of-tree kinds gain their namespace (field_notes.summarize), closing the collision where two external extensions sharing a local class name collided on settings keys, dedup slots, DBOS workflow names, and step prefixes. Explicit kind is a local suffix; dotted values are rejected.
  • Per-run extension state deleted. start(extension=), the attributes["extension"] stamp, the self.extension instance attr, the _emit_run_event/_log_run_event threading, and Run.extension (the 647bc7f4 read-back) are all gone. Lifecycle events derive their extension from the run's kind through the registry inside the memoized _transition step — deterministic class metadata, replay-safe.
  • Transcript file listing returns names, not URLs. The client already composes the listing URL from (extension, callId); it now composes file URLs the same way (subjectApi.transcriptFile). AgentCallFiles.from_call is a pure filesystem inventory — no data reads in the schema method.

Plan deviation

The plan seeded druks.core/druks.usage as framework packages resolving to None — but core and usage are builtin extensions with entry points, so they register through the same flow as build and their workflows resolve extension == "core" / "usage". No seeding, no special case; None remains only for explicitly registered unowned packages (test modules).

Deploy requirement

_entry dropped its third argument. DBOS recovery re-invokes recorded arguments, so no runs may be in flight when this deploys — drain or cancel live runs first. In-tree kinds and step names are unchanged, so completed checkpoints stay replay-compatible.

Historical attributes["extension"] values in DBOS rows become ignored write-orphans; no migration.

🤖 Generated with Claude Code

czpython and others added 6 commits July 13, 2026 11:04
start() stamps {subject_type, subject_id, extension} as DBOS custom
workflow attributes at enqueue (subject_id always a string), so every
runs-for-a-subject query reads workflow_status itself through one
EXISTS predicate (subject_filter). The subject, extension, and input
columns leave durable_runs; the row keeps only the facts DBOS has no
slot for: the gate ask, failure, and timestamps.

Events and signals take subject/extension from the workflow's own
arguments (replay-deterministic), the park notification receives the
subject from the parking workflow, and the transcript-files route names
its own mount instead of reading the run's stamp. The migration runs
DBOS's system migrations first so the attributes backfill from
durable_runs always has its column, whatever the host booted last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every host already runs dbos 2.26, so the attributes column exists
before this history does, and runs predating the deploy stay unkeyed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run.list_for_subject grows a kind filter; build's newest-scope lookup
and Scope.parked_for compose it instead of importing the raw
subject_filter predicate from the durable engine. Per (kind, subject)
the queue dedup makes runs strictly sequential, so newest-first holds
within a kind and at most one run is parked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The declaring extension namespaces every workflow's kind (registered by the
loader before extension modules import), so extension resolves from the class:
start() loses its extension argument, the DBOS attributes stamp subject only,
lifecycle events derive their extension from the run's kind, and the transcript
file listing returns names the client composes into URLs it already knows.

Deploy note: _entry dropped its third argument — drain or cancel in-flight
runs before deploying; DBOS recovery re-invokes recorded arguments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…names

An aliased entry point (key != Extension.name) no longer poisons the package
ownership map: the pre-import claim is provisional — released when the load
fails, and an already-held claim is never touched, so the name-mismatch check
raises the promised MalformedExtension in both orders. The two transcript-files
tests missed in the url removal now assert names and compose the download URL
the way the client does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@czpython
czpython changed the base branch from commonzenpython/eng-630-sdk-adopt-dbos-custom-workflow-attributes-for-subject-keying to main July 13, 2026 15:54
czpython and others added 6 commits July 13, 2026 18:05
Subject is routing metadata and rides before the payload everywhere else
(start, _execute_run, the event emitters) — the entry chain was the one
deviator, flipped inside the same compatibility window the arity change
already opened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry raising ValueError forced the loader to catch-and-rebrand at a
distance; a conflicting claim IS a packaging mistake, so the registry raises
the loader's taxonomy itself and _resolve flattens to a bare with-block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entry modules never declare workflows — capability modules import at
discover()/load(app), after the loader returns — so nothing needs the package
registered before the entry loads. Registering the validated name with the
authoritative Extension.package deletes the claim context manager, the
derived-package guess, and the two-pass boot loop, and a failed load registers
nothing by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package map is the loader-validated install claim, so it moves into the
loader — registry.py returns to the capability catalogs it always was, and
workflows.py asks the loader who owns a module. iter_extensions() gains the
entry-key/name check _resolve() already had: production boot no longer accepts
an aliased entry the app-less path rejects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generic-subjects seed and the scope-dispatch assertion still carried the
per-run extension; the duplicate-name boot test now aliases both entries to one
key, since per-entry name validation fires before duplication is observable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@czpython
czpython merged commit 30a1175 into main Jul 13, 2026
2 checks passed
@czpython
czpython deleted the extension-class-identity branch July 13, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant